<?php
/**
 * Created by PhpStorm.
 * User: 王崇全
 * Date: 2018-12-21
 * Time: 10:03
 */

namespace app\common\model;

use think\Db;
use think\facade\Cache;

/**
 * 无限极分类的基类
 * 可用于 任何数型结构
 * 据测试, 目前的性能瓶颈在一万级左右
 * @author  王崇全
 * @date    2018/12/21 10:03
 * @package app\common\model
 */
class Catalog
{

    //目录类型
    const TYPE = [
        'article',
        'config',
        'user',
        'adminer',
    ];

    protected $table = ''; // 子类传过来的表名称

    /**
     * Catalog constructor.
     * @param string $tableName 数据表名称
     * @throws
     */
    public function __construct(string $tableName)
    {
        $this->table = $tableName;

        if (!$this->table) {
            throw new \Exception("请设置数据表名称");
        }
    }

    /**
     * 拾取器
     * @author 王崇全
     * @date   2018/12/21 10:24
     * @param $value
     * @param $data
     * @return bool
     */
    protected function getIsLeafNodeAttr($value, $data)
    {
        return $data["ln"] - $data["rn"] == 1 ? true : false;
    }

    /**
     * 修改器
     * @author 王崇全
     * @date
     * @param $value
     * @param $data
     * @return mixed
     * @throws \Exception
     */
    protected function setTypeAttr($value, $data)
    {
        if (!in_array($value, self::TYPE)) {
            throw new \Exception("目录类型错误");
        }

        return $value;
    }


    /**
     * 添加目录
     * @author 王崇全
     * @date
     * @param string $title 目录名
     * @param string $pgid  父目录ID
     * @return string 返回新增的id
     * @throws
     */
    public function add(string $title, string $pgid = null)
    {
        if (!$pgid) {
            $pgrn = Db::name($this->table)->max("rn") + 1;
            if (!$pgrn) {
                $pgrn = 2;
            }
        } else {
            $pgrn = Db::name($this->table)
                ->where('id', $pgid)
                ->value("rn");
        }
        if (!$pgrn) {
            throw new \Exception("此父目录不存在");
        }
        Db::startTrans();
        try {
            Db::name($this->table)
                ->where("ln", ">=", $pgrn)
                ->setInc("ln", 2);
            Db::name($this->table)
                ->where("rn", ">=", $pgrn)
                ->setInc("rn", 2);

            $new = Db::name($this->table)->insertGetId([
                "ln"    => $pgrn,
                "rn"    => $pgrn + 1,
                "pid"   => $pgid ?? "",
                "title" => $title ?? "",
            ]);
            if (!$new) {
                throw new \Exception("目录添加失败");
            }

            Db::commit();
        } catch (\Exception $e) {
            Db::rollback();
            throw new \Exception($e->getMessage());
        }

        $this->clearCache();

        return $new;
    }

    /**
     * 编辑目录
     * @author 王崇全
     * @date
     * @param string      $gid   目录ID
     * @param string|null $title 目录名称
     * @param string|null $pgid  父目录ID
     * @return string 返回受影响的行数
     * @throws
     */
    public function edit(string $gid, string $title = null, string $pgid = null)
    {
        $data = [];
        if (isset($title)) {
            $data["title"] = $title;
        }

        Db::startTrans();
        try {
            if (isset($pgid)) {
                if ($pgid == $gid) {
                    throw new \Exception("父目录不能是自己");
                }

                $this->ChangeParentGroup($gid, $pgid);
            }
            $rows = Db::name($this->table)
                ->where('id', $gid)
                ->update($data);
            Db::commit();
        } catch (\Exception $e) {
            Db::rollback();
            throw new \Exception($e->getMessage());
        }
        $this->clearCache();

        return $rows;
    }

    /**
     * 删除目录
     * @author 王崇全
     * @date
     * @param string $gid 目录ID
     * @return int 返回受影响的行数
     * @throws
     */
    public function del(string $gid)
    {
        Db::startTrans();
        try {
            $info = Db::name($this->table)
                ->field([
                    "ln" => "ln",
                    "rn" => "rn",
                    "rn-ln+1 AS width",
                ])
                ->where("id", $gid)
                ->find();
            if (!$info) {
                throw new \Exception("此目录不存在");
            }
            $lcode = $info['ln'];
            $rcode = $info['rn'];
            $width = $info['width'];
            $rows  = Db::name($this->table)
                ->whereBetween('ln', "$lcode,$rcode")
                ->delete();
            Db::name($this->table)
                ->where("ln", ">", $lcode)
                ->setDec("ln", $width);
            Db::name($this->table)
                ->where("rn", ">", $rcode)
                ->setDec("rn", $width);

            Db::commit();
        } catch (\Exception $e) {
            Db::rollback();
            throw new \Exception($e->getMessage());
        }

        $this->clearCache();

        return $rows;
    }

    /**
     * 平移目录
     * @author 王崇全
     * @date
     * @param string $gid 目录ID
     * @param string $pos 目标位置的目录ID
     * @return void
     * @throws
     */
    public function moveTo(string $gid, string $pos)
    {
        if ($gid == $pos) {
            return;
        }

        Db::startTrans();

        $info = $this->GetNodeInfo($gid);
        if (!$info) {
            throw new \Exception("此目录不存在");
        }

        $infoBeforeGroup = $this->GetNodeInfo($pos);
        if (!$infoBeforeGroup) {
            throw new \Exception("目标位置目录不存在");
        }

        if ($info["pid"] != $infoBeforeGroup["pid"]) {
            throw new \Exception("只能平级移动");
        }

        $distance = $info["ln"] - $infoBeforeGroup["ln"];

        //是否是向左移动
        $toLeft = $distance > 0 ? true : false;

        //获取此节点的所有子节点
        $ln     = $info['ln'];
        $rn     = $info['rn'];
        $groups = Db::name($this->table)
            ->whereBetween('ln', "$ln,$rn")
            ->column('id');
        try {
            //移动目录
            if ($toLeft) {
                //s1 顺移其他目录
                Db::name($this->table)
                    ->where("ln", ">=", $infoBeforeGroup["ln"])
                    ->where("ln", "<", $info["ln"])
                    ->setInc("ln", $info["width"]);

                Db::name($this->table)
                    ->where("rn", ">", $infoBeforeGroup["ln"])
                    ->where("rn", "<", $info["ln"])
                    ->setInc("rn", $info["width"]);

                //s2 移动本目录
                Db::name($this->table)
                    ->whereIn('id', $groups)
                    ->setDec("ln", $distance);

                Db::name($this->table)
                    ->whereIn('id', $groups)
                    ->setDec("rn", $distance);
            } else {
                $distance = $infoBeforeGroup["rn"] - $info["rn"];

                //s1 顺移其他目录
                Db::name($this->table)
                    ->where("ln", "<", $infoBeforeGroup["rn"])
                    ->where("ln", ">", $info["rn"])
                    ->setDec("ln", $info["width"]);

                Db::name($this->table)
                    ->where("rn", ">", $info["rn"])
                    ->where("rn", "<=", $infoBeforeGroup["rn"])
                    ->setDec("rn", $info["width"]);

                //s2 移动本目录
                Db::name($this->table)
                    ->whereIn('id', $groups)
                    ->setInc("ln", $distance);

                Db::name($this->table)
                    ->whereIn('id', $groups)
                    ->setInc("rn", $distance);
            }

            Db::commit();
        } catch (\Exception $e) {
            Db::rollback();
            throw new \Exception($e->getMessage());
        }

        $this->clearCache();
    }

    /**
     * 修改父目录
     * @author 王崇全
     * @date
     * @param string $gid  目录id
     * @param string $pgid 父目录id
     * @return void
     * @throws
     */
    public function changeParentGroup(string $gid, string $pgid)
    {

        //父节点不能是自己
        if ($gid == $pgid) {
            throw new \Exception("父节点不能是自己");
        }

        Db::startTrans();

        //节点信息
        $info = $this->GetNodeInfo($gid);
        if (!$info) {
            throw new \Exception("节点不存在");
        }

        //父节点没有改变
        if ($info["pid"] == $pgid) {
            Db::commit();

            return;
        }

        //获取此节点的所有子节点
        $ln     = $info['ln'];
        $rn     = $info['rn'];
        $groups = Db::name($this->table)
            ->whereBetween('ln', "$ln,$rn")
            ->column('id');

        $pgid = trim($pgid);
        if ($pgid === "" || $pgid === '""' || $pgid === "''") { // 父节点为根节点
            $maxCode = Db::name($this->table)->max("rn");
            $d       = $maxCode - $info["rn"];

            try {
                //s1 节点右侧的所有左右值左移
                Db::name($this->table)
                    ->where("ln", ">=", $info["rn"] + 1)
                    ->setDec("ln", $info["width"]);
                Db::name($this->table)
                    ->where("rn", ">=", $info["rn"] + 1)
                    ->setDec("rn", $info["width"]);

                //s2 节点右移
                Db::name($this->table)
                    ->whereIn('id', $groups)
                    ->setInc('ln', $d);
                Db::name($this->table)
                    ->whereIn('id', $groups)
                    ->setInc('rn', $d);

                //s3 修改自身的父目录ID
                Db::name($this->table)
                    ->where('id', $gid)
                    ->update(["pid" => ""]);

            } catch (\Exception $e) {
                Db::rollback();
                throw new \Exception($e->getMessage());
            }

        } else { // 父节点不是根节点

            //父节点信息
            $pInfo = Db::name($this->table)
                ->field([
                    "pid" => "pid",
                    "ln"  => "ln",
                    "rn"  => "rn",
                    "rn - ln + 1 AS width",
                ])
                ->where('id', $pgid)
                ->find();
            if (!$pInfo) {
                throw new \Exception("父节点不存在");
            }

            //父节点不能是自己的子节点
            if ($pInfo["ln"] > $info["ln"] && $pInfo["rn"] < $info["rn"]) {
                throw new \Exception("父节点不能是自己的子节点");
            }

            //是否是向左移动
            $toLeft = $info["rn"] - $pInfo["rn"] > 0 ? true : false;

            if ($toLeft) { //向左移动

                //s1 右移中间的左右值

                //要改变左右值的最小边界
                $min = Db::name($this->table)
                    ->where('id', $pgid)
                    ->value("rn");

                //要改变左右值的最大边界
                $max = $info["ln"];

                //要移动的距离
                $width = $info["width"];

                try {
                    //右移中间的左值
                    Db::name($this->table)
                        ->where("ln", ">", $min)
                        ->where("ln", "<", $max)
                        ->setInc("ln", $width);

                    //右移中间的右值
                    Db::name($this->table)
                        ->where("rn", ">=", $min)
                        ->where("rn", "<", $max)
                        ->setInc("rn", $width);

                    //s2 移动自身

                    //要移动的距离
                    $distance = $info["ln"] - $pInfo["rn"];

                    //左移左值
                    Db::name($this->table)
                        ->whereIn('id', $groups)
                        ->setInc('ln', $distance);
                    //左移右值
                    Db::name($this->table)
                        ->whereIn('id', $groups)
                        ->setInc('rn', $distance);
                    //s3 修改自身的父目录ID
                    Db::name($this->table)
                        ->where('id', $gid)
                        ->update(["pid" => $pgid]);

                } catch (\Exception $e) {
                    Db::rollback();
                    throw new \Exception($e->getMessage());
                }

            } else { //向右移动

                //s1 左移中间的左右值

                //要改变左右值的最小边界
                $min = $info["rn"] + 1;

                //要改变左右值的最大边界
                $max = Db::name($this->table)
                        ->where('id', $pgid)
                        ->value("rn") - 1;

                //要移动的距离
                $width = $info["width"];

                try {
                    //左移中间的左值
                    Db::name($this->table)
                        ->where("ln", ">=", $min)
                        ->where("ln", "<=", $max)
                        ->setDec("ln", $width);

                    //左移中间的右值
                    Db::name($this->table)
                        ->where("rn", ">=", $min)
                        ->where("rn", "<=", $max)
                        ->setDec("rn", $width);

                    //s2 移动自身

                    //要移动的距离
                    $distance = $pInfo["rn"] - $info["rn"] - 1;

                    //右移左值
                    Db::name($this->table)
                        ->whereIn('id', $groups)
                        ->setInc('ln', $distance);
                    //右移右值
                    Db::name($this->table)
                        ->whereIn('id', $groups)
                        ->setInc('rn', $distance);
                    //s3 修改自身的父目录ID
                    Db::name($this->table)
                        ->where('id', $gid)
                        ->update(["pid" => $pgid]);

                } catch (\Exception $e) {
                    Db::rollback();
                    throw new \Exception($e->getMessage());
                }
            }
        }

        Db::commit();

        $this->clearCache();
    }

    /**
     * 目录列表-仅包含直接子节点
     * @author 王崇全
     * @date
     * @param string|null $pgid         父目录ID
     * @return array|null 没有数据返回 [] array[
     *                                  'id' =>
     *                                  'title' => '目录名称',
     *                                  'child' => '子节点'
     *                                  ]
     * @throws
     */
    public function getList(string $pgid = null)
    {
        $map = [];
        if (isset($pgid)) {
            $map[] = [
                'pid',
                '=',
                $pgid,
            ];
        } else {
            $map[] = [
                'pid',
                '=',
                '',
            ];
        }

        $list = Db::name($this->table)
            ->field('id,title')
            ->where($map)
            ->order('ln')
            ->select();
        if (!$list) {
            return [];
        }

        return $list;
    }

    /**
     * 目录列表- 包含所有子节点
     * 注意, 会有数据过大的风险, 慎用
     * @author 王崇全
     * @param int    $pid        父级id
     * @param string $sonKeyName 子节点的键名
     * @return array 所有子节点的多维数组
     * @throws
     */
    public function getListTree(int $pid, string $sonKeyName = "son")
    {
        //父节点信息
        $pcInfo = Db::name($this->table)
            ->field([
                "ln",
                "rn",
            ])
            ->where("id", $pid)
            ->find();

        if (!$pcInfo) {
            throw new \Exception("此目录不存在");
        }

        //所有子节点
        $posterity = Db::name($this->table)
            ->field([
                "id",
                "pid",
                "title",
            ])
            ->where("ln", ">", $pcInfo["ln"])
            ->where("rn", "<", $pcInfo["rn"])
            ->select();
        if (!$posterity) {
            return [];
        }

        //转为树型多维数组
        return $this->generateTree($posterity, $sonKeyName);
    }

    /**
     * 获取目录信息
     * @author 王崇全
     * @date
     * @param string $gid 目录id
     * @return array [
     *                    'pid' => '上级id',
     *                    'title' => '目录名称'
     *                    ]
     * @throws
     */
    public function info(string $gid)
    {
        $info = Db::name($this->table)
            ->field('pid,title')
            ->where('id', $gid)
            ->find();
        if (!$info) {
            return [];
        }

        return $info;
    }

    /**
     * 获取兄弟目录的名称
     * @author 王崇全
     * @date
     * @param string $pgid 父目录ID
     * @return array [
     *                     'title' => '目录名称'
     *                     ]
     */
    public function gettitlesByPgid(string $pgid = "")
    {
        return Db::name($this->table)
            ->where('pid', $pgid)
            ->column("title");
    }

    /**
     * 获取目录ID集
     * @author 王崇全
     * @date
     * @param string $pgid 父目录ID
     * @return array
     * @throws
     */
    public function getIdsByPgid(string $pgid = "")
    {
        $code = Db::name($this->table)
            ->field('ln,rn')
            ->where('id', $pgid)
            ->find();

        return Db::name($this->table)
            ->where('ln', '>=', $code['ln'])
            ->where('rn', '<=', $code['rn'])
            ->column('id');
    }

    /**
     * 获取目录名称
     * @author 王崇全
     * @date
     * @param string $gid 目录ID
     * @return string 返回title 目录名称
     */
    public function getTitle(string $gid)
    {
        return Db::name($this->table)
            ->where('id', $gid)
            ->value("title");
    }

    /**
     * 查看目录是否存在
     * @author 王崇全
     * @date
     * @param string $id
     * @return bool 成功返回 true 否则返回 false
     */
    public function checkExist(string $id)
    {
        return Db::name($this->table)
            ->where('id', $id)
            ->value("id", false) ? true : false;
    }

    /**
     * 按照路径依次检查目录名是否存在
     * @author 王崇全
     * @date
     * @param array $titlePath 各级目录的名称构成的数组
     * @return string|null 存在，返回最末级目录的ID；否则，null
     */
    public function checkExistBytitle(array $titlePath)
    {
        $id = null;

        while ($title = array_shift($titlePath)) {
            $id = Db::name($this->table)
                ->where('title', $title)
                ->where('pid', $id ?? "")
                ->value('id', null);
            if (!$id) {
                return $id;
            }
        }

        return $id;
    }

    /**
     * 获取上级的ID
     * @author 王崇全
     * @date
     * @param string $id
     * @return mixed 返回pid 上级id
     */
    public function getParentGroupID(string $id)
    {
        return Db::name($this->table)
            ->where('id', $id)
            ->value("pid");
    }


    /**
     * 数组转树
     * @author 王崇全
     * @date   2018/12/21 11:08
     * @param array  $data      待转化的一维数组
     * @param string $sonKeyName 子树的键名
     * @param string $pidKeyName 一维数组中 id的键名
     * @param string $idKeyName  一维数组中 父id的键名
     * @return array 树型的多维数组
     */
    protected function generateTree(array $data, string $sonKeyName = "son", string $pidKeyName = "pid", string $idKeyName = "id")
    {
        $items = [];
        foreach ($data as $key => $val)
            $items[$val['id']] = $val;

        $tree = [];

        foreach ($items as $item) {
            if (isset($items[$item[$pidKeyName]])) {
                $items[$item[$pidKeyName]][$sonKeyName][] =& $items[$item[$idKeyName]];
            } else {
                $tree[] =& $items[$item[$idKeyName]];
            }
        }

        return $tree;
    }


    /**
     * 获取节点信息
     * @author 王崇全
     * @date
     * @param string $gid 目录id
     * @return null|array [
     *                    'pid' => '上级id',
     *                    'ln' => '左值',
     *                    'rn' => '右值',
     *                    'width' => ''
     *                    ]
     * @throws
     */
    public function getNodeInfo(string $gid)
    {
        return Db::name($this->table)
            ->field([
                "pid",
                "ln",
                "rn",
                "rn - ln + 1 AS width",
            ])
            ->where('id', $gid)
            ->find();
    }

    /**
     * 清除缓存
     * @author 王崇全
     * @date   2018/12/21 10:07
     * @return void
     */
    private function clearCache()
    {
        Cache::clear();
    }

}